/*   The MIT License
 *   
 *   Carina Engine
 *   Copyright (c) 2009 2010 2011 2012 Zdravko Velinov
 *   
 *   Permission is hereby granted, free of charge, to any person obtaining a copy
 *   of this software and associated documentation files (the "Software"), to deal
 *   in the Software without restriction, including without limitation the rights
 *   to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 *   copies of the Software, and to permit persons to whom the Software is
 *   furnished to do so, subject to the following conditions:
 *
 *   The above copyright notice and this permission notice shall be included in
 *   all copies or substantial portions of the Software.
 *
 *   THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 *   IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 *   FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 *   AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 *   LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 *   OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 *   THE SOFTWARE.
 */

#include "carina/shader-program.hh"
#include "carina/csp-driver.hh"
#include "carina/csp-ast.hh"
#include "carina/resources.hh"

#include <algorithm>

namespace Carina
{
ShaderProgramSet::ShaderProgramSet(ResourceManager& res_mgr, const string& filename)
#ifdef CE_RELOADABLE_RESOURCES
    :   m_FileName(filename)
#endif
{
    CSP::Driver csp_driver;
    csp_driver.parse(filename);
    const AST::List* root_node = csp_driver.getASTRoot()->extract<AST::List>();
    if(root_node)
    {
        for(AST::List::const_iterator i = root_node->current(); i != root_node->end(); ++i)
            switch(i->getNodeType())
            {
            case CSP::CE_CSP_PROGRAM:
            {
                auto* program = i->extract<CSP::Program>();
                auto* program_params = program->getBody();
                ShadingLanguagesList shlang_list;
                for(auto j = program_params->current(); j != program_params->end(); ++j)
                {
                    CE_ASSERT(j->getNodeType() == CSP::CE_CSP_PARAMETER, "unsupported node type in the following file: " + filename);
                    auto* prog_val = j->extract<CSP::Parameter>();
                    shlang_list.push_back(prog_val->getNodeName());
                }
                auto compiler = res_mgr.getRenderer()->getPreferredShaderCompiler(shlang_list);
                for(auto j = program_params->current(); j != program_params->end(); ++j)
                {
                    auto* prog_val = j->extract<CSP::Parameter>();
                    if(prog_val->getNodeName() == compiler->getName())
                    {
                        auto prog_path = res_mgr.getResource(prog_val->getValue()
                            CE_RELOAD_CALLBACK(std::bind(&ShaderProgramSet::reloadShader, this, std::placeholders::_1)));
                        m_Programs[program->getNodeName()] = compiler->compileShaderProgram(prog_path);
                        break;
                    }
                }
            } break;
            case CSP::CE_CSP_MATERIAL:
            {
                string                          sprog;
                const CSP::CommonVariables*     common_variables;
                DataVector<const CSP::Profile*> profiles;
                auto* material = i->extract<CSP::Material>();
                for(auto j = material->getBody()->current(); j != material->getBody()->end(); ++j)
                {
                    switch(j->getNodeType())
                    {
                    case CSP::CE_CSP_PARAMETER:
                    {
                        auto* param = j->extract<CSP::Parameter>();
                        if(param->getNodeName() != "shader_program")
                            THROW_EXCEPTION("unsupported parameter: " + param->getNodeName() + ": " + filename);
                        sprog = param->getValue();
                    } break;
                    case CSP::CE_CSP_COMMON_VARIABLES:
                    {
                        if(common_variables)
                            THROW_EXCEPTION("second common variables definition is forbidden: " + filename);
                        common_variables = j->extract<CSP::CommonVariables>();
                    } break;
                    case CSP::CE_CSP_PROFILE:
                    {
                        if(common_variables)
                            THROW_EXCEPTION("profile definition after common variables definition is forbidden: " + filename);
                        profiles.push_back(j->extract<CSP::Profile>());
                    } break;
                    }
                }
                if(sprog.empty())
                    THROW_EXCEPTION("no shader program is specified: " + filename);
                auto prog = m_Programs.find(sprog);
                if(prog == m_Programs.end())
                    THROW_EXCEPTION("unknown program \"" + sprog + "\" specified in the following material declaration: " + material->getNodeName() + ": " + filename);
                Material mat(prog->second);
                for(auto i = profiles.begin(); i != profiles.end(); ++i)
                {
                    MaterialProfile map;
                    loadVariables((*i)->getBody(), prog->second, map);
                    mat.addProfile((*i)->getNodeName(), map);
                }
                if(common_variables)
                {
                    MaterialProfile map;
                    loadVariables(common_variables->getBody(), prog->second, map);
                    mat.addVariables(map);
                }
                m_Materials[material->getNodeName()] = mat;
            } break;
            default:
                assert(false); break;
            }
    }
}
ShaderProgramSet::~ShaderProgramSet()
{
}

ShaderProgramPtr ShaderProgramSet::getShaderProgram(const string& name)
{
    auto prog = m_Programs.find(name);
    if(prog == m_Programs.end())
        THROW_EXCEPTION("unknown shader program: " + name);
    return prog->second;
}

ShaderProgramPtr ShaderProgramSet::getShaderProgram()
{
    if(m_Programs.empty())
        THROW_EXCEPTION("no shader programs are available in this set");
    if(m_Programs.size() > 1)
        THROW_EXCEPTION("more than one shader program is available in this set");
    return m_Programs.begin()->second;
}

size_t ShaderProgramSet::getShaderProgramCount() const
{
    return m_Programs.size();
}

void ShaderProgramSet::getMaterial(const string& name, Material& material) const
{
    auto i = m_Materials.find(name);
    if(i == m_Materials.end())
        THROW_EXCEPTION("unknown material: " + name);
    material = i->second;
}

void ShaderProgramSet::getMaterial(Material& material) const
{
    if(m_Materials.empty())
        THROW_EXCEPTION("no materials are available in this shader program set");
    if(m_Materials.size() > 1)
        THROW_EXCEPTION("more than one material is available in this shader program set");
    material = m_Materials.begin()->second;
}

void ShaderProgramSet::loadVariables(const AST::List* _list, const ShaderProgramPtr& shader, MaterialProfile& map)
{
    string var_name;
    UniformVarTypeDesc desc;

    for(auto i = _list->current(); i != _list->end(); ++i)
        switch(i->getNodeType())
        {
        case CSP::CE_CSP_VARIABLE:
        {
            var_name = i->extract<CSP::Variable>()->getNodeName();
            shader->getVariableType(var_name, desc);
            addUniformValue(map.variables, var_name, desc.type, desc.size);
        } break;
        case CSP::CE_CSP_TEXTURE:
        {
            addUniformValue(map.textures, i->extract<CSP::Texture>()->getNodeName(), CE_UVT_TEXTURE, 1);
        } break;
        default:
            assert(false); break;
        }
}

#ifdef CE_RELOADABLE_RESOURCES
bool ShaderProgramSet::reloadShader(const string& name)
{
    string filename = Path(name).filename();
    size_t programs_recompiled = 0;
    CSP::Driver csp_driver;
    csp_driver.parse(m_FileName);
    const AST::List* root_node = csp_driver.getASTRoot()->extract<AST::List>();
    if(root_node)
    {
        for(AST::List::const_iterator i = root_node->current(); i != root_node->end(); ++i)
            if(i->getNodeType() == CSP::CE_CSP_PROGRAM)
            {
                auto* program = i->extract<CSP::Program>();
                auto program_params = program->getBody();
                for(auto j = program_params->current(); j != program_params->end(); ++j)
                {
                    if(j->getNodeType() != CSP::CE_CSP_PARAMETER)
                        THROW_EXCEPTION("Unsupported node type in the following file: " + filename);
                    auto* prog_val = j->extract<CSP::Parameter>();
                    if(prog_val->getValue() == filename)
                    {
                        auto iter = m_Programs.find(program->getNodeName());
                        iter->second->reload(filename);
                        return true; // TODO: Materials
                    }
                }
            }
    }
    return programs_recompiled > 0;
}
#endif
}